/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.factory;

import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.javaruntype.type.Type;
import org.jfab.core.model.CGAnnotation;
import org.jfab.core.model.CGClass;
import org.jfab.core.model.CGCompilationUnit;
import org.jfab.core.model.CGConstructor;
import org.jfab.core.model.CGEnum;
import org.jfab.core.model.CGField;
import org.jfab.core.model.CGInterface;
import org.jfab.core.model.CGMethod;
import org.jfab.core.model.CGMethodDeclaration;
import org.jfab.core.model.CGObject;
import org.jfab.core.model.CGParameter;
import org.jfab.core.model.CGType;
import org.jfab.core.model.CGTypeUtilities;

/**
 * Provides support for type references for the code generator.
 */
public final class DefaultTypeReferenceSupport implements TypeReferenceSupport
{
    /** Types to ignore. */
    private final Set<CGType> _ignoreTypes;

    /** Package name. */
    private String _packageName;

    /** Referenced types. */
    private final Set<CGType> _referencedTypes;

    /** Code generator type utilities. */
    private final CGTypeUtilities _cgTypeUtils;

    /** Type factory. */
    private final CGTypeFactory _cgTypeFactory;

    /**
     * Construct this object with the given parameters.
     * 
     * @param packageName Package name.
     * @param cgTypeUtils Code generator type utilities. (required)
     * @param cgTypeFactory Code generator type factory. (required)
     * @param typeComparator Code generator type comparator.
     */
    public DefaultTypeReferenceSupport(final String packageName,
            final CGTypeUtilities cgTypeUtils,
            final CGTypeFactory cgTypeFactory,
            final Comparator<CGType> typeComparator)
    {
        Validate.notNull(cgTypeUtils, "cgTypeUtils is null");
        Validate.notNull(cgTypeFactory, "cgTypeFactory is null");

        _packageName = packageName;
        _cgTypeUtils = cgTypeUtils;
        _cgTypeFactory = cgTypeFactory;

        _ignoreTypes = createIgnoreTypes(typeComparator);
        _referencedTypes = new TreeSet<CGType>(typeComparator);
    }

    @Override
    public final void addReferencedType(final CGType cgType)
    {
        if ((cgType != null) && !_ignoreTypes.contains(cgType))
        {
            _referencedTypes.add(cgType);
            addReferencedTypes(cgType.getTypeParameters());
        }
    }

    @Override
    public final void addReferencedTypes(final Collection<CGType> cgTypes)
    {
        if (CollectionUtils.isNotEmpty(cgTypes))
        {
            for (final CGType cgType : cgTypes)
            {
                addReferencedType(cgType);
            }
        }
    }

    @Override
    public final void clear()
    {
        _referencedTypes.clear();
    }

    @Override
    public final String createComponentTypeString(final CGType cgType)
    {
        String answer = "void";

        if (cgType != null)
        {
            final Type<?> type = _cgTypeUtils.getType(cgType);

            if ((type != null) && (type.getRawClass() == Void.class))
            {
                answer = "void";
            }
            else
            {
                if (_cgTypeUtils.isInJavaLangPackage(cgType)
                        || _referencedTypes.contains(cgType))
                {
                    answer = _cgTypeUtils.toSimpleComponentNameString(cgType);
                }
                else
                {
                    answer = _cgTypeUtils.toComponentNameString(cgType);
                }

                if (answer != null)
                {
                    answer = answer.replaceAll("java.lang.", "");
                    answer = answer.replaceAll(_packageName + ".", "");
                }
            }
        }

        return answer;
    }

    @Override
    public final String createTypeString(final CGType cgType)
    {
        String answer = "void";

        if (cgType != null)
        {
            final Type<?> type = _cgTypeUtils.getType(cgType);

            if ((type != null) && (type.getRawClass() == Void.class))
            {
                answer = "void";
            }
            else
            {
                if (_cgTypeUtils.isInJavaLangPackage(cgType)
                        || _referencedTypes.contains(cgType))
                {
                    answer = _cgTypeUtils.toSimpleNameString(cgType);
                }
                else
                {
                    answer = _cgTypeUtils.toNameString(cgType);
                }

                if (answer != null)
                {
                    answer = answer.replaceAll("java.lang.", "");
                    if (answer.startsWith(_packageName)
                            && (StringUtils.countMatches(answer, ".") == StringUtils
                                    .countMatches(_packageName, ".")))
                    {
                        answer = answer.replaceAll(_packageName + ".", "");
                    }
                }
            }
        }

        return answer;
    }

    @Override
    public final void determineReferencedTypes(final CGClass cgClass)
    {
        determineReferencedTypes((CGObject)cgClass);

        addReferencedType(cgClass.getExtendsType());
        addReferencedTypes(cgClass.getImplementsInterfaces());

        for (final CGConstructor cgConstructor : cgClass.getConstructors())
        {
            determineReferencedTypes(cgConstructor);
        }

        for (final CGField cgField : cgClass.getFields())
        {
            determineReferencedTypes(cgField);
        }

        for (final CGMethod cgMethod : cgClass.getMethods())
        {
            determineReferencedTypes(cgMethod);
        }
    }

    @Override
    public final void determineReferencedTypes(final CGConstructor cgConstructor)
    {
        determineReferencedTypes((CGObject)cgConstructor);

        for (final CGParameter cgParameter : cgConstructor.getParameters())
        {
            addReferencedType(cgParameter.getType());
        }

        addReferencedTypes(cgConstructor.getThrowsTypes());
    }

    @Override
    public final void determineReferencedTypes(final CGEnum cgEnum)
    {
        determineReferencedTypes((CGObject)cgEnum);

        for (final CGConstructor cgConstructor : cgEnum.getConstructors())
        {
            determineReferencedTypes(cgConstructor);
        }

        for (final CGField cgField : cgEnum.getFields())
        {
            determineReferencedTypes(cgField);
        }

        for (final CGMethod cgMethod : cgEnum.getMethods())
        {
            determineReferencedTypes(cgMethod);
        }
    }

    @Override
    public final void determineReferencedTypes(final CGField cgField)
    {
        determineReferencedTypes((CGObject)cgField);

        addReferencedType(cgField.getType());
    }

    @Override
    public final void determineReferencedTypes(final CGInterface cgInterface)
    {
        determineReferencedTypes((CGObject)cgInterface);

        addReferencedTypes(cgInterface.getExtendsTypes());

        for (final CGField cgField : cgInterface.getFields())
        {
            determineReferencedTypes(cgField);
        }

        for (final CGMethodDeclaration cgMethod : cgInterface.getMethods())
        {
            determineReferencedTypes(cgMethod);
        }
    }

    @Override
    public final void determineReferencedTypes(final CGMethod cgMethod)
    {
        determineReferencedTypes((CGMethodDeclaration)cgMethod);
    }

    @Override
    public final void determineReferencedTypes(
            final CGMethodDeclaration cgMethod)
    {
        determineReferencedTypes((CGObject)cgMethod);

        addReferencedType(cgMethod.getReturnType());

        for (final CGParameter cgParameter : cgMethod.getParameters())
        {
            addReferencedType(cgParameter.getType());
        }

        addReferencedTypes(cgMethod.getThrowsTypes());
    }

    @Override
    public Set<String> getImportTypeNames(
            final CGCompilationUnit cgCompilationUnit)
    {
        final Set<String> answer = new TreeSet<String>();

        final Set<CGType> cgTypes = getReferencedTypes();

        for (final CGType cgType : cgTypes)
        {
            if (!_cgTypeUtils.isInJavaLangPackage(cgType)
                    && !_cgTypeUtils.isArray(cgType)
                    && !_cgTypeUtils.isInDefaultPackage(cgType)
                    && !_cgTypeUtils.isTypeParameter(cgType))
            {
                final String nameString = _cgTypeUtils
                        .toComponentNameString(cgType);

                if (nameString.contains("."))
                {
                    answer.add(nameString);
                }
            }
        }

        return answer;
    }

    @Override
    public final String getPackageName()
    {
        return _packageName;
    }

    @Override
    public final Set<CGType> getReferencedTypes()
    {
        return _referencedTypes;
    }

    @Override
    public final void removeReferencedType(final CGType cgType)
    {
        _referencedTypes.remove(cgType);
    }

    @Override
    public final void setPackageName(final String packageName)
    {
        Validate.notEmpty(packageName, "packageName is empty");

        _packageName = packageName;
    }

    /**
     * @param typeComparator Code generator type comparator.
     * 
     * @return the ignore types.
     */
    private Set<CGType> createIgnoreTypes(
            final Comparator<CGType> typeComparator)
    {
        final Set<CGType> answer = new TreeSet<CGType>(typeComparator);

        answer.add(_cgTypeFactory.createType(boolean.class));
        answer.add(_cgTypeFactory.createType(byte.class));
        answer.add(_cgTypeFactory.createType(double.class));
        answer.add(_cgTypeFactory.createType(float.class));
        answer.add(_cgTypeFactory.createType(int.class));
        answer.add(_cgTypeFactory.createType(long.class));

        return answer;
    }

    /**
     * Determine the types referenced by the given parameter.
     * 
     * @param cgObject Code generator object.
     */
    private void determineReferencedTypes(final CGObject cgObject)
    {
        addReferencedTypes(cgObject.getReferencedTypes());

        for (final CGAnnotation annotation : cgObject.getAnnotations())
        {
            addReferencedType(annotation.getType());
        }
    }
}
